// TODO: CODE REVIEW & CLEANUP!
//
// SunfireHardware.cs
//
// Implements HomeUX.Components.SunfireHardware.
//

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using HomeUX.Utilities;

namespace HomeUX.Components
{

/// <summary>
/// Controls Sunfire Theater Grand Processor II hardware.
/// </summary>
///
/// <remarks>
/// 
/// General usage pattern:
///   1. Construct an instance of SunfireHardware.
///   2. Hook up events.
///   3. Call Open (inherited from SimpleSerialHardware).
///   4. To control the hardware, you can use TransmitCommand inherited from SimpleSerialHardware)
///      to send a raw command to the serial port, but in general it's easier to use higher-level
///      control methods like SetVolume and SetSource.  These methods construct and
///      send a serial command and, when the reply is received, they parse the hardware's reply and
///      call a callback method you provide, providing the parsed reply object as an argument.
///
/// Philosophy about exceptions:
///   -- Programmer errors can lead to exceptions like ArgumentException.
///   -- Hardware problems (including timeouts) won't lead to exceptions, but rather will cause the
///      following to occur:
///        -- Higher-level control methods (see above) will be called with null in place of the
///           parsed reply object.  This lets you clean up your state.
///        -- CommandFailed, WarningReported events will be fired as appropriate.  This lets you
///           centralize reporting of hardware problems to the user.
/// </remarks>
///
[DebuggerDisplay("SunfireHardware {HardwareLabel}")]
public class SunfireHardware : SimpleSerialHardware<GenericReply>
{
    const string UnexpectedOutputMessage = "Unexpected output from hardware: {0}";

	static readonly string[] _inputNames =
	{
		// IMPORTANT: order must be the same as SunfireInputId
		"CD",
		"DAT/Tape",
		"DBS/BS",
		"LD/DVD",
		"Phone",
		"Tuner",
		"V.Aux",
		"VCR1",
		"VCR2"
		// IMPORTANT: order must be the same as SunfireInputId
	};

	static readonly string[] _effectNames =
	{
		// IMPORTANT: order must be the same as SunfireEffectId
		"Stereo",
		"Pro Logic",
		"3-Stereo",
		"Cathedral",
		"Stadium",
		"Jazz Club"
		// IMPORTANT: order must be the same as SunfireEffectId
	};

    public static IList<string> InputNames
    {
        get
        {
            return _inputNames;
        }
    }

    public static IList<string> EffectNames
    {
        get
        {
            return _effectNames;
        }
    }

    public event EventHandler<SunfireHardwareWarningsReportedEventArgs> WarningsReported;

    public SunfireHardware(WorkQueue workQueue, VirtualSerialPort serialPort, string hardwareLabel) :
        base(workQueue, serialPort, SimpleSerialHardwareMode.Line, hardwareLabel)
    {
        serialPort.NewLine = "\r";
    }

    /// <summary>
    /// Returns a given volume level but restricts it to the range accepted by the hardware, i.e.
    /// 0 to 99 inclusive.
    /// </summary>
    ///
    /// <param name="volume">The given volume level.</param>
    ///
    public int BoundVolume(int volume)
    {
        return Math.Max(0, Math.Min(99, volume));
    }

    public void SetIsOn(bool isOn, Action<GenericReply> onComplete)
    {
        TransmitCommand((isOn ? "*112" : "*113"), onComplete);
    }

    public void SetIsPowered(bool isPowered, Action<GenericReply> onComplete)
    {
        TransmitCommand((isPowered ? "*112" : "*113"), onComplete);
    }

    // volume is 0 to 99
    public void SetVolume(int volume, Action<GenericReply> onComplete)
    {
        TransmitCommand(
            String.Format(CultureInfo.InvariantCulture, "*11U{0:00}", BoundVolume(volume)),
            onComplete);
	}

    public void SetIsMuted(bool isMuted, Action<GenericReply> onComplete)
    {
        TransmitCommand((isMuted ? "*11Q" : "*11R"), onComplete);
    }

    public void SetSource(SunfireInputId inputId, Action<GenericReply> onComplete)
    {
        string command;
        switch (inputId)
        {
		case SunfireInputId.Cd:			command = "*114"; break;
		case SunfireInputId.DatTape:	command = "*115"; break;
		case SunfireInputId.DbsBs:		command = "*116"; break;
		case SunfireInputId.LdDvd:		command = "*117"; break;
		case SunfireInputId.Phono:		command = "*118"; break;
		case SunfireInputId.Tuner:		command = "*119"; break;
		case SunfireInputId.VAux:		command = "*11A"; break;
		case SunfireInputId.Vcr1:		command = "*11B"; break;
		case SunfireInputId.Vcr2:		command = "*11C"; break;
        default:
			throw new ArgumentException("Invalid SunfireInputId", "inputId");
        }

        TransmitCommand(command, onComplete);
    }

    public void SetEffect(SunfireEffectId effectId, Action<GenericReply> onComplete)
    {
        string command;
        switch (effectId)
        {
		case SunfireEffectId.Stereo:		command = "*11E"; break;
		case SunfireEffectId.ProLogic:		command = "*11F"; break;
		case SunfireEffectId.ThreeStereo:	command = "*11G"; break;
		case SunfireEffectId.Cathedral:		command = "*11H"; break;
		case SunfireEffectId.Stadium:		command = "*11J"; break;
		case SunfireEffectId.JazzClub:		command = "*11K"; break;
        default:
			throw new ArgumentException("Invalid SunfireEffectId", "effectId");
        }

        TransmitCommand(command, onComplete);
    }

    public void SetIsHolo(bool isHolo, Action<GenericReply> onComplete)
    {
        TransmitCommand((isHolo ? "*11M" : "*11N"), onComplete);
    }

    public static SunfireInputId ParseInputId(string str)
    {
        for (SunfireInputId inputId = 0; (int)inputId < _inputNames.Length; inputId++)
        {
            if (_inputNames[(int)inputId] == str)
                return inputId;
        }

        throw new ArgumentException(String.Format(
            "Expected a Sunfire input ID, got \"{0}\"; valid IDs are: {1}",
            str, String.Join(", ", _inputNames)));
    }

    public static string FormatInputId(SunfireInputId inputId)
    {
        if ((inputId >= 0) && ((int)inputId < _inputNames.Length))
            return _inputNames[(int)inputId];
        else
            return inputId.ToString();
    }

    public static SunfireEffectId ParseEffectId(string str)
    {
        for (SunfireEffectId effectId = 0; (int)effectId < _effectNames.Length; effectId++)
        {
            if (_effectNames[(int)effectId] == str)
                return effectId;
        }

        throw new ArgumentException(String.Format(
            "Expected a Sunfire effect ID, got \"{0}\"; valid IDs are: {1}",
            str, String.Join(", ", _effectNames)));
    }

    public static string FormatEffectId(SunfireEffectId effectId)
    {
        if ((effectId >= 0) && ((int)effectId < _effectNames.Length))
            return _effectNames[(int)effectId];
        else
            return effectId.ToString();
    }

    // base class overrides

    protected override void ParseReceivedString(string received, ref string pushBack,
        string currentCommand, Action<GenericReply> onReply)
    {
		// keep track of whether the current command (if any) has completed
		bool currentCommandCompleted = false;

        // if we come across miscellaneous issue(s) to report (e.g. the driver sends text we
        // don't expect), <warnings> will be set to the issue(s) to report, otherwise it
        // will be null
        LazyStringList warnings = new LazyStringList();

        // ignore line feeds
        received = received.Replace("\n", String.Empty);

        // if this driver previously sent a command to the hardware, look for an echo of the
		// command -- no other reply information is sent by the hardware
        if (currentCommand != null)
        {
            // search the received string for the first occurrence of "<currentCommand>\r"
            string find = currentCommand + "\r";
            int index = received.IndexOf(find);
            if (index >= 0)
            {
                // this is the success case -- the hardware received the command and echoed it
                // back with a terminating carriage return
				currentCommandCompleted = true;

                // everything before or after the echoed command is reported as unexpected
                // hardware output and ignored
                if (index > 0)
                {
                    warnings.Add(String.Format(UnexpectedOutputMessage,
                        received.Substring(0, index)));
                }
                if (index + find.Length < received.Length)
                {
                    warnings.Add(String.Format(UnexpectedOutputMessage,
                        received.Substring(index + find.Length)));
                }

                // update driver state to indicate that we're no longer executing a command
                EndCommand();
            }
        }

        // report warnings, if any
        if ((warnings.Count > 0) && (WarningsReported != null))
        {
            WarningsReported(this, new SunfireHardwareWarningsReportedEventArgs()
            {
                Messages = warnings.ListOrNull
            });
        }

		if (currentCommandCompleted)
		{
			// call the pending command's callback function
			onReply(GenericReply.Reply);
		}
        else
            pushBack = received;
    }
}

/// <summary>
/// Identifies an input to the Sunfire hardware, as labeled on the hardware itself.
/// </summary>
public enum SunfireInputId
{
	// IMPORTANT: order must be the same as <_inputNames>

	/// <summary>
	/// Input "CD".
	///</summary>
	Cd,

	/// <summary>
	/// Input "DAT/TAPE".
	///</summary>
	DatTape,

	/// <summary>
	/// Input "DBS/BS".
	///</summary>
	DbsBs,

	/// <summary>
	/// Input "LD/DVD".
	///</summary>
	LdDvd,

	/// <summary>
	/// Input "PHONO".
	///</summary>
	Phono,

	/// <summary>
	/// Input "TUNER".
	///</summary>
	Tuner,

	/// <summary>
	/// Input "V.AUX".
	///</summary>
	VAux,

	/// <summary>
	/// Input "VCR1".
	///</summary>
	Vcr1,

	/// <summary>
	/// Input "VCR2".
	///</summary>
	Vcr2

	// IMPORTANT: order must be the same as <_inputNames>
}

/// <summary>
/// Identifies an audio processing effect implemented by the Sunfire hardware.
/// </summary>
public enum SunfireEffectId
{
	// IMPORTANT: order must be the same as <_effectNames>

	/// <summary>
	/// Effect "STEREO".
	/// </summary>
	Stereo,

	/// <summary>
	/// Effect "PRO LOGIC".
	/// </summary>
	ProLogic,

	/// <summary>
	/// Effect "3-STEREO".
	/// </summary>
	ThreeStereo,

	/// <summary>
	/// Effect "CATHEDRAL".
	/// </summary>
	Cathedral,

	/// <summary>
	/// Effect "STADIUM".
	/// </summary>
	Stadium,

	/// <summary>
	/// Effect "JAZZ-CLUB".
	/// </summary>
	JazzClub

	// IMPORTANT: order must be the same as <_effectNames>
}

[DebuggerDisplay("Count = {Messages.Count}")]
public class SunfireHardwareWarningsReportedEventArgs : EventArgs
{
    public ICollection<string> Messages { get; internal set; }
}

}

